package app.gui;

import java.awt.Color;
import java.awt.Cursor;
import java.awt.Graphics;
import java.awt.Point;
import java.awt.Polygon;
import java.awt.Rectangle;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.util.LinkedList;
import java.util.List;

import javax.swing.JPanel;

import app.util.LinearInterpolation;

public class PointDiagram extends JPanel implements Runnable, MouseListener, MouseMotionListener {
	/**
	 * 
	 */
	private static final long serialVersionUID = 903734667528031182L;
	// POINTS CONTAINS VALUES IN DIAGRAM, NOT AN ABSOLUTE POSITION
	// Diagram items
	private List<Point> points;		// List with all points 
	private int snapPoint;	// currently snapped point
	private int movedPoint;	// currently moved point
	private int pointRadius = 5;	// circle radius which represents points
	private int snapZoneRadius = pointRadius + 2;	// half length of snap zone square 

	// cursor position
	private int cursorX, cursorY;	// current cursor position
	private Rectangle pointArea;	// area, where can be added point

	// store component size to check resize
	int width, height;

	// upper and lower diagram polygon created from points and borders
	private Polygon upperPoly;
	private Polygon lowerPoly;

	// diagram drawing constants
	private int xOffset = 35; // x axis distance from border
	private int yOffset = 35; // y axis distance from border
	private int itemSize = 5; // item size
	private int minX = 0;	// axis values
	private int maxX = 50;
	private int minY = 0;
	private int maxY = 100;
	private int xItemCount = 20; // item count on both axis
	private int yItemCount = 20;

	private int xItemOffset;  // distance between items
	private int yItemOffset;

	// component color list
	private Color backgroundColor = Color.blue;
	private Color axisColor = Color.white;
	private Color textColor = Color.white;
	private Color pointColor = Color.white;
	private Color upperColor = Color.green;
	private Color lowerColor = Color.red;

	/**
	 * Class constructor
	 * 
	 * register all listeners, set cursor for this component and start new thread
	 * 
	 */
	public PointDiagram() {
		super();

		// init point area
		pointArea = new Rectangle();

		// init point list
		points = new LinkedList<Point>();

		// add first and last points
		Point firstPoint = new Point(minX, minY);
		points.add(firstPoint);
		Point lastPoint = new Point(maxX, maxY);
		points.add(lastPoint);

		// create polygons
		upperPoly = new Polygon();
		lowerPoly = new Polygon();

		// add mouse listeners
		addMouseListener(this);
		addMouseMotionListener(this);

		// set crosshair cursor
		setCursor(new Cursor(Cursor.CROSSHAIR_CURSOR));

		// start new thread for this component
		Thread t = new Thread(this);
		t.start();
	}

	//=============================================================================================================================
	//	Computing engine
	//=============================================================================================================================

	/**
	 * Thread running method
	 * 
	 * Component computing engine
	 * 
	 */
	public void run() {
		while(true) {
			// check resize
			if(isResized()) {
				// recount distance between items
				xItemOffset = Math.round((getWidth() - (float)(2 * xOffset)) / (float)(xItemCount));
				yItemOffset = Math.round((getHeight() - (float)(2 * yOffset)) / (float)(yItemCount));

				// recount point area rectangle
				pointArea.setFrameFromDiagonal(getXPos(minX) + 1, getYPos(minY) - 1, getXPos(maxX) - 1, getYPos(maxY) + 1);

				// recount upper and lower polygons
				updatePolygons();

			}
			movedPoint += checkPoint(movedPoint);

			// check snap zones
			snapPoint = checkSnapZones();

			// paint chart
			repaint();

			// sleep for a while
			try {
				Thread.sleep(40);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}


	/**
	 * Component was resized
	 * 
	 * @return True, if component was resized
	 */
	private boolean isResized() {
		boolean resized = false;
		if(getWidth() != width) {
			width = getWidth();
			resized = true;
		}
		if(getHeight() != height) {
			height = getHeight();
			resized = true;
		}
		return resized;
	}

	/**
	 * If cursor is in snap zone, return index of this point
	 * 
	 * @return Snapped point index
	 */
	private synchronized int checkSnapZones() {
		Rectangle zone = new Rectangle();
		for(Point p : points) {
			zone.x = getXPos(p.x) - snapZoneRadius;
			zone.y = getYPos(p.y) - snapZoneRadius;
			zone.width = snapZoneRadius * 2;
			zone.height = snapZoneRadius * 2;
			if(zone.contains(cursorX, cursorY))
				return points.indexOf(p); 
		}
		return -1;
	}

	/**
	 * Renew diagram polygons 
	 */
	private synchronized void updatePolygons() {
		upperPoly.reset();
		upperPoly.addPoint(getXPos(maxX), getYPos(maxY));
		upperPoly.addPoint(getXPos(minX), getYPos(maxY));
		for(Point p : points) {
			upperPoly.addPoint(getXPos(p.x), getYPos(p.y));
		}
		lowerPoly.reset();
		lowerPoly.addPoint(getXPos(maxX), getYPos(minY));
		lowerPoly.addPoint(getXPos(minX), getYPos(minY));
		for(Point p : points) {
			lowerPoly.addPoint(getXPos(p.x), getYPos(p.y));
		}

	}

	//=============================================================================================================================
	//	Draw component
	//=============================================================================================================================

	/**
	 * Component painting engine
	 */
	public void paint(Graphics g) {
		// draw background
		g.setColor(backgroundColor);
		g.fillRect(0, 0, getWidth(), getHeight());

		// draw upper - lower polygons
		g.setColor(lowerColor);
		g.fillPolygon(lowerPoly);
		g.setColor(upperColor);
		g.fillPolygon(upperPoly);

		// X - axis 
		drawXAxis(g);

		// Y - axis
		drawYAxis(g);

		// draw all points
		drawPoints(g);


	}

	/**
	 * Draw X axis in diagram
	 * 
	 * @param g Component canvas
	 */
	private void drawXAxis(Graphics g) {
		int xPos;
		// draw lower X axis line
		g.setColor(axisColor);
		g.drawLine(xOffset, getHeight() - yOffset, getWidth() - xOffset, getHeight() - yOffset);
		// draw axis items
		for(int i = 0; i < xItemCount; i++) {
			xPos = xOffset + i * xItemOffset;
			g.setColor(axisColor);
			g.drawLine(xPos, getHeight() - yOffset, xPos, (getHeight() - yOffset) + itemSize);
			g.setColor(textColor);
			g.drawString(Integer.toString(getXValue(xPos)), xPos - 5, getHeight() - 5);
		}
		// draw last item
		xPos = getWidth() - xOffset - 1;
		g.setColor(axisColor);
		g.drawLine(xPos, getHeight() - yOffset, xPos, (getHeight() - yOffset) + itemSize);
		g.setColor(textColor);
		g.drawString(Integer.toString(getXValue(xPos)), xPos - 5, getHeight() - 5);

		// draw upper X axis line
		g.setColor(axisColor);
		g.drawLine(xOffset, yOffset, getWidth() - xOffset, yOffset);
		// draw axis items
		for(int i = 0; i < xItemCount; i++) {
			xPos = xOffset + i * xItemOffset;
			g.setColor(axisColor);
			g.drawLine(xPos, yOffset, xPos, yOffset - itemSize);
			g.setColor(textColor);
			g.drawString(Integer.toString(getXValue(xPos)), xPos - 5, yOffset - itemSize - 5);
		}
		// draw last item
		xPos = getWidth() - xOffset - 1;
		g.setColor(axisColor);
		g.drawLine(xPos, yOffset, xPos, yOffset - itemSize);
		g.setColor(textColor);
		g.drawString(Integer.toString(getXValue(xPos)), xPos - 5, yOffset - itemSize - 5);


	}

	/**
	 * Draw Y axis in diagram
	 * 
	 * @param g Component canvas
	 */
	private void drawYAxis(Graphics g) {
		int yPos;
		// draw left Y axis line
		g.setColor(axisColor);
		g.drawLine(xOffset, yOffset, xOffset, getHeight() - yOffset);
		// draw items
		for(int i = 0; i < yItemCount; i++) {
			yPos = getHeight() - yOffset - i * yItemOffset;
			g.setColor(axisColor);
			g.drawLine(xOffset, yPos, xOffset - itemSize, yPos);
			g.setColor(textColor);
			g.drawString(Integer.toString(getYValue(yPos)), 5, yPos);
		}
		// draw last item; 
		yPos = yOffset + 1;
		g.setColor(axisColor);
		g.drawLine(xOffset, yPos, xOffset - itemSize, yPos);
		g.setColor(textColor);
		g.drawString(Integer.toString(getYValue(yPos)), 5, yPos);

		// draw right Y axis line
		g.setColor(axisColor);
		g.drawLine(getWidth() - xOffset, yOffset, getWidth() - xOffset, getHeight() - yOffset);
		// draw items
		for(int i = 0; i < yItemCount; i++) {
			yPos = getHeight() - yOffset - i * yItemOffset;
			g.setColor(axisColor);
			g.drawLine(getWidth() - xOffset, yPos, getWidth() - xOffset + itemSize, yPos);
			g.setColor(textColor);
			g.drawString(Integer.toString(getYValue(yPos)), getWidth() - xOffset + itemSize + 5, yPos);
		}
		// draw last item; 
		yPos = yOffset + 1;
		g.setColor(axisColor);
		g.drawLine(getWidth() - xOffset, yPos, getWidth() - xOffset + itemSize, yPos);
		g.setColor(textColor);
		g.drawString(Integer.toString(getYValue(yPos)), getWidth() - xOffset + itemSize + 5, yPos);

	}

	/**
	 * Draw all points in point list
	 * 
	 * @param g Component canvas
	 */
	private void drawPoints(Graphics g) {
		g.setColor(pointColor);
		// draw first item
		Point p = points.get(0);
		g.drawOval(getXPos(p.x) - pointRadius, getYPos(p.y) - pointRadius, pointRadius * 2, pointRadius * 2);
		// draw other items
		for(int i = 1; i < points.size(); i++) {
			p = points.get(i);
			g.drawOval(getXPos(p.x) - pointRadius, getYPos(p.y) - pointRadius, pointRadius * 2, pointRadius * 2);
			g.drawLine(getXPos(points.get(i - 1).x), getYPos(points.get(i - 1).y), getXPos(p.x), getYPos(p.y));
		}

		// fill snapped point
		if(snapPoint > -1) 
			g.fillOval(getXPos(points.get(snapPoint).x) - pointRadius, getYPos(points.get(snapPoint).y) - pointRadius, pointRadius * 2, pointRadius * 2);
	}



	//=============================================================================================================================
	//	Linear interpolation
	//=============================================================================================================================

	/**
	 * Position method
	 * 
	 * @param x_pos Position in diagram on X axis
	 * @return Axis value
	 */
	private int getXValue(int x_pos) {
		return LinearInterpolation.interpolate(xOffset, getWidth() - xOffset, minX, maxX, x_pos);
	}

	/**
	 * Position method
	 * 
	 * @param x_value Value on X axis in diagram
	 * @return Position on X axis
	 */
	private int getXPos(int x_value) {
		return LinearInterpolation.interpolate(minX, maxX, xOffset, getWidth() - xOffset, x_value);
	}

	/**
	 * Position method
	 * 
	 * @param y_pos Position in diagram on Y axis
	 * @return Axis value
	 */
	private int getYValue(int y_pos) {
		return LinearInterpolation.interpolate(getHeight() - yOffset, yOffset, minY, maxY, y_pos);
	}

	/**
	 * Position method
	 * 
	 * @param y_value Value on Y axis in diagram
	 * @return Position on Y axis
	 */
	private int getYPos(int y_value) {
		return LinearInterpolation.interpolate(minY, maxY, getHeight() - yOffset, yOffset, y_value);
	}

	//=============================================================================================================================
	//	Point action
	//=============================================================================================================================

	/**
	 * Add new point into diagram
	 * 
	 * 
	 * @param x Added point x position
	 * @param y Added point y position
	 */
	private void addPoint(int x, int y) {
		Point p = new Point(x, y);
		if(!pointArea.contains(getXPos(x), getYPos(y)))
			return;
		int index = -1;
		for(Point pi : points) {
			if(pi.x > p.x) {
				index = points.indexOf(pi);
				points.add(index, p);
				break;
			}
		}
	}

	/**
	 * Remove point from diagram
	 * 
	 * @param index Removed point
	 */
	private void removePoint(int index) {
		points.remove(index);
	}

	/**
	 * Sort point list after point position change
	 * uses bubblesort-like algorithm
	 * 
	 * @param index Changed point index in point list
	 * @return Position change coeficient
	 */
	private synchronized int checkPoint(int index) {
		if(index > 0 && index < points.size() - 1) {
			Point p = points.get(index);
			while(!(p.x >= points.get(index - 1).x && p.x <= points.get(index + 1).x)) {
				if(p.x > points.get(index + 1).x) {
					swapPoints(index, index + 1);
					index++;
					p = points.get(index);
					return 1;
				}
				else { 
					swapPoints(index - 1, index);
					index--;
					p = points.get(index);
					return -1;
				}
			}
		}
		return 0;
	}

	/**
	 * Check all points positions after mouse button release
	 * 
	 */
	private void checkAllPoints() {
		for(int j = 0; j < points.size(); j++)
			for(int i = 1; i < points.size(); i++) {
				checkPoint(i);
			}
	}

	/**
	 * Swap two neighbor point x positions in point list
	 * 
	 * @param i1 First point index
	 * @param i2 Second point index
	 */
	private void swapPoints(int i1, int i2) {
		int x = points.get(i1).x;
		int y = points.get(i1).y;
		points.get(i1).x = points.get(i2).x;
		points.get(i1).y = points.get(i2).y;
		points.get(i2).x = x;
		points.get(i2).y = y;
	}

	//=============================================================================================================================
	//	Mouse action
	//=============================================================================================================================

	@Override
	public void mouseDragged(MouseEvent e) {
		if(e.getModifiers() == MouseEvent.BUTTON1_MASK) {
			cursorX = e.getX();
			cursorY = e.getY();
			if(pointArea.contains(cursorX, cursorY))
				if((movedPoint == 0) || (movedPoint == points.size() - 1))
					points.get(movedPoint).y = getYValue(cursorY);
				else if(movedPoint > 0) { 
					points.get(movedPoint).x = getXValue(cursorX);
					points.get(movedPoint).y = getYValue(cursorY);
				}
			movedPoint += checkPoint(movedPoint);
			updatePolygons();
		}
	}

	@Override
	public void mouseMoved(MouseEvent e) {
		cursorX = e.getX();
		cursorY = e.getY();
	}

	@Override
	public void mouseClicked(MouseEvent e) {
		cursorX = e.getX();
		cursorY = e.getY();
	}

	@Override
	public void mousePressed(MouseEvent e) {
		if(e.getButton() == MouseEvent.BUTTON1) {
			cursorX = e.getX();
			cursorY = e.getY();
			movedPoint = snapPoint;
			if(movedPoint < 0) 
				addPoint(getXValue(cursorX), getYValue(cursorY)); 

		}
		if(e.getButton() == MouseEvent.BUTTON3 && snapPoint > 0 && snapPoint < points.size() - 1) {
			removePoint(snapPoint);
		}
		updatePolygons();
	}

	@Override
	public void mouseReleased(MouseEvent e) {
		cursorX = e.getX();
		cursorY = e.getY();
		if(e.getButton() == MouseEvent.BUTTON1) {
			if(pointArea.contains(cursorX, cursorY))
				if(movedPoint == 0)
					points.get(0).y = getYValue(cursorY);
				else if(movedPoint == points.size() - 1)
					points.get(movedPoint).y = getYValue(cursorY);
				else if(movedPoint > 0) { 
					points.get(movedPoint).x = getXValue(cursorX);
					points.get(movedPoint).y = getYValue(cursorY);
				}
			movedPoint += checkPoint(movedPoint);
			checkAllPoints();
			updatePolygons();
		}
	}

	@Override
	public void mouseEntered(MouseEvent e) {
		cursorX = e.getX();
		cursorY = e.getY();
	}

	@Override
	public void mouseExited(MouseEvent e) {
		cursorX = e.getX();
		cursorY = e.getY();
	}
}
